package system;

import java.io.EOFException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.SocketException;
import java.security.KeyStore;
import java.util.Properties;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import javax.net.ssl.SSLSocket;

import record.Record;
import record.UserRecord;
import request.AllProjectsRequest;
import request.ChangeCredentialsRequest;
import request.LogoutRequest;
import request.NewProjectRequest;
import request.NewSessionRequest;
import request.NewUserRequest;
import request.ProjectRecordRequest;
import request.RemoveProjectRequest;
import request.SaveRequest;

/**
 * @author Sean Freitag
 * 
 * Contains both the main and object intended to operate the server.
 * 
 * ------Warning------
 * In order for the SSL sockets to work, the included cacerts file (in the root directory) must be installed
 * for the testKeys to work.
 * -------------------
 */
public class Server implements Runnable{
	
	/**
	 * Socket used to connect to a client
	 */
	private SSLSocket socket;
	
	/**
	 * Output stream for sending Records
	 */
	private ObjectOutputStream out;
	
	/**
	 * Input stream for receiving Requests
	 */
	private ObjectInputStream in;
	
	/**
	 * Server log
	 */
	private Log log;
	
	/**
	 * Session log
	 */
	private Log userLog;
	
	/**
	 * Server socket for generating sockets
	 */
	private static SSLServerSocket serverSocket;
	
	/**
	 * Once a user is logged in, user defines a valid session
	 */
	private String user;
	
	/**
	 * 
	 */
	private Database database;

	/**
	 * Main function for the Server
	 * @param args
	 * No arguments are accepted
	 */
	public static void main(String args[]){
				
		try {
			//SSL is initialized
			initializeServerSocket();
		} catch (Exception e) {
			//No need to run if SSL fails
			System.out.println(e.toString());
			System.exit(-1);
		}
		
		//No exit routine is implemented
		while(true){
		
			try {
				//Accept a new client
				//Hangs until a client connects
				SSLSocket sock = getSocket();
				
				//Start a new thread
				new Thread(new Server(sock)).start();
			}catch (Exception e) {
				//This shouldn't happen, but if it does, it would be nice to know why
				e.printStackTrace();
			}
		
		}
		
	}

	/**
	 * Initializes a new server to interact with each client
	 * @param socket
	 * Connected socket for communcation
	 * @throws IOException
	 * Thrown when the socket state is unexpected, 
	 * mostly due to when the client disconnects without informing the server
	 */
	public Server(SSLSocket socket) throws IOException{
		this.socket = socket;
		this.log = new Log("Server", true);
		out = new ObjectOutputStream(socket.getOutputStream());
		in  = new ObjectInputStream(socket.getInputStream());
	}
	
	/**
	 * Intended to be run as a new thread for interacting with clients
	 * @see java.lang.Runnable#run()
	 */
	public void run() {
		
		this.database = new LocalData();
		
		audit("Connected");
		
		//Request holder
		Object request = null;
		
		try {
			//accept a new request and process it
			while(true){
				out.reset();
				request = in.readObject();
				
				if(request == null)
					audit("null Request");
				else
					audit(request.toString());
				
				if(request == null)
					sendToClient(null);
				else if(request instanceof NewUserRequest){
					sendToClient(database.addUser((NewUserRequest)request));
				}else if(request instanceof NewSessionRequest){
					UserRecord record = database.newSession((NewSessionRequest)request);
					//session validation starts here
					if(record != null) user = record.getEmailAddress();
					sendToClient(record);
				}else if(request instanceof NewProjectRequest)
					sendToClient(database.newProject(user, (NewProjectRequest)request));
				else if(request instanceof ProjectRecordRequest)
					sendToClient(database.getProject(user, (ProjectRecordRequest)request));
				else if(request instanceof LogoutRequest){
					sendToClient(null);
					//ends thread
					break;
				}
				else if(request instanceof AllProjectsRequest){
					((AllProjectsRequest) request).setUser(user);
					sendToClient(database.getAllProjects((AllProjectsRequest)request));
				}else if(request instanceof ChangeCredentialsRequest)
					sendToClient(database.changeCredentials(user, (ChangeCredentialsRequest)request));
				else if(request instanceof RemoveProjectRequest){
					((RemoveProjectRequest) request).setUser(user);
					sendToClient(database.removeProject((RemoveProjectRequest)request));
				}else if(request instanceof SaveRequest)
					sendToClient(database.save(user, (SaveRequest)request));
				else
					audit("Unauthorized Request");
				
			}

			//these exceptions are expected and happen under normal operations
		}catch (EOFException e){
		
		}catch (SocketException e){
			//if a new exception happens, we should know what
		}catch (Exception e){
			audit(e.toString());
		}
		
		audit("Disconnected");
		
	}
	
	/**
	 * Sends an object to a client
	 * The method is mostly to control auditing
	 * @param record
	 * The record to be returned to the client
	 * @throws IOException
	 * If the socket cannot submit the Record
	 */
	private void sendToClient(Record record) throws IOException{
		if(record == null)
			audit("null record");
		else{
			audit(record.toString());
		}
		out.writeObject(record);
	}
	
	/**
	 * Logs all major events in the server
	 * Used for diagnosing issues
	 * Time stamps and records current socket
	 * @param string
	 * Line to be added to the log
	 */
	private void audit(String string){
		if(user != null)
			if(userLog == null)
				try {
					userLog = new Log(user);
					audit("Login " + user);
				} catch (Exception e) {
					e.printStackTrace();
				}
			else
				userLog.write(socket.getRemoteSocketAddress().toString() + " | " + string);

		log.write(socket.getRemoteSocketAddress().toString() + " | " + string);
	}

	/**
	 * @return
	 * A newly generated Socket
	 * @throws IOException 
	 * Socket issues are a dime a dozen
	 */
	private static SSLSocket getSocket() throws IOException{
		return (SSLSocket) serverSocket.accept();
	}
	
	
	/**
	 * Initializes ServerSocket for generating client sockets
	 * @throws Exception
	 * Many different errors can occur, but if this function does not work,
	 * there is no need to run the server
	 */
	private static void initializeServerSocket() throws Exception {
		SSLServerSocketFactory ssf;
		SSLContext ctx;
		KeyManagerFactory kmf;
		KeyStore ks;
		
		char[] passphrase = "passphrase".toCharArray();

		ctx = SSLContext.getInstance("TLS");
		kmf = KeyManagerFactory.getInstance("SunX509");
		ks = KeyStore.getInstance("JKS");
	
		Properties props = new Properties();
		try
		{
			props.load(new FileInputStream("resources/config.cfg"));
		}
		catch (IOException e)
		{
			throw new IOException("Properties file cannot be found or there was an error reading it.", e);
		}
		
		//ks.load(new FileInputStream("src/system/testkeys"), passphrase);
		ks.load(new FileInputStream(props.getProperty("keys")), passphrase);
		kmf.init(ks, passphrase);
		ctx.init(kmf.getKeyManagers(), null, null);
	
		ssf = ctx.getServerSocketFactory();
		try
		{
    //		serverSocket = (SSLServerSocket) ssf.createServerSocket(CommunicationProtocol.port);
    		serverSocket = (SSLServerSocket) ssf.createServerSocket(Integer.parseInt(props.getProperty("port")));
    		serverSocket.setNeedClientAuth(true);
		}
		catch (NumberFormatException e)
		{
			throw new RuntimeException("Config file missing port value, or port value is invalid");
		}
	}
	

}
